﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Threading;
using System.Timers;
using System.Diagnostics;
using System.Windows.Threading;
using System.Windows.Media.Animation;
using System.Reflection;

namespace Master
{
    public abstract class Car : Image
    {
        /*****************
         * STATIC FIELDS *
         *****************/
        private static readonly int sleepTime = 40;

        /*************************
         * DEPENDENCY PROPERTIES *
         *************************/
        public static readonly DependencyProperty directionProperty, positionProperty, orientationProperty, speedProperty, scaleProperty;

        public Direction Direction
        {
            set { SetValue(directionProperty, value); }
            get { return (Direction)GetValue(directionProperty); }
        }

        public Point Position
        {
            set { SetValue(positionProperty, value); }
            get { return (Point)GetValue(positionProperty); }
        }

        public int Orientation
        {
            set { SetValue(orientationProperty, value); }
            get { return (int)GetValue(orientationProperty); }
        }

        public double Speed
        {
            set { SetValue(speedProperty, value); }
            get { return (double)GetValue(speedProperty); }
        }

        public double Scale
        {
            set { SetValue(scaleProperty, value); }
            get { return (double)GetValue(scaleProperty); }
        }

        private static void positionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (Map.INSTANCE.ActualHeight == 0) return;
            Car thisCar = (Car)d; Rect bounds = thisCar.getTrueBounds();
            CarLeftMapEventArgs newEvent = null;
            if (bounds.X > Map.INSTANCE.ActualWidth && thisCar.Orientation >= 0 && thisCar.Orientation <= 180)
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.RIGHT);
            else if (bounds.X < 0 - bounds.Width && thisCar.Orientation >= 180)
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.LEFT);
            else if (bounds.Y > Map.INSTANCE.ActualHeight && thisCar.Orientation >= 90 && thisCar.Orientation <= 270)
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.BOTTOM);
            else if (bounds.Y < 0 - bounds.Height && (thisCar.Orientation <= 90 || thisCar.Orientation >= 270))
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.TOP);
            if (newEvent != null) thisCar.RaiseEvent(newEvent);
        }

        private static void orientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Car thisCar = (Car)d;
            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(new RotateTransform((int)e.NewValue));
            transformGroup.Children.Add(new ScaleTransform(thisCar.Scale, thisCar.Scale));
            thisCar.RenderTransform = transformGroup;
        }

        /**********
         * EVENTS *
         **********/
        public static readonly RoutedEvent CarLeftMapEvent; //--> GETS RAISED WHEN CAR LEAVES SCREEN

        public event CarLeftMapEventHandler CarLeftMap
        {
            add { AddHandler(Car.CarLeftMapEvent, value); }
            remove { RemoveHandler(Car.CarLeftMapEvent, value); }
        }

        /**********************
         * STATIC CONSTRUCTOR *
         **********************/
        static Car()
        {
            //DEPENDENCY PROPERTIES METADATA
            FrameworkPropertyMetadata positionPropertyMetadata = new FrameworkPropertyMetadata(new Point(-500, -500),
                new PropertyChangedCallback(Car.positionPropertyChanged));
            FrameworkPropertyMetadata orientationPropertyMetadata = new FrameworkPropertyMetadata(0,
                new PropertyChangedCallback(Car.orientationPropertyChanged));
            FrameworkPropertyMetadata speedPropertyMetadata = new FrameworkPropertyMetadata((double)10);
            FrameworkPropertyMetadata scalePropertyMetadata = new FrameworkPropertyMetadata((double)0.5);
            //DEPENDENCY PROPERTIES
            directionProperty = DependencyProperty.Register("Direction", typeof(Direction), typeof(Car));
            positionProperty = DependencyProperty.Register("Position", typeof(Point), typeof(Car), positionPropertyMetadata);
            orientationProperty = DependencyProperty.Register("Orientation", typeof(int), typeof(Car), orientationPropertyMetadata);
            speedProperty = DependencyProperty.Register("Speed", typeof(double), typeof(Car), speedPropertyMetadata);
            scaleProperty = DependencyProperty.Register("Scale", typeof(double), typeof(Car), scalePropertyMetadata);
            //EVENTS
            Car.CarLeftMapEvent = EventManager.RegisterRoutedEvent("CarLeftMap",
                RoutingStrategy.Direct, typeof(CarLeftMapEventHandler), typeof(Car));
        }

        /***************
         * CONSTRUCTOR *
         ***************/
        public Car()
        {
            initTransformOrigin();
            initCarImage();
            initRenderTransforms();
            bindPosition();


            Canvas.SetZIndex(this, 2);
        }

        /******************
         * INITIALIZATION *
         ******************/
        private void initTransformOrigin()
        {
            RenderTransformOrigin = new Point(0.5, 0.5);
        }

        protected abstract void initCarImage();

        protected void initRenderTransforms()
        {
            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(new RotateTransform(Orientation));
            transformGroup.Children.Add(new ScaleTransform(Scale, Scale));
            RenderTransform = transformGroup;
        }

        private void bindPosition()
        {
            Binding Xbinding = new Binding();
            Xbinding.Source = this;
            Xbinding.Path = new PropertyPath("Position.X");
            Xbinding.Mode = BindingMode.TwoWay;
            this.SetBinding(Map.LeftProperty, Xbinding);

            Binding Ybinding = new Binding();
            Ybinding.Source = this;
            Ybinding.Path = new PropertyPath("Position.Y");
            Ybinding.Mode = BindingMode.TwoWay;
            this.SetBinding(Map.TopProperty, Ybinding);
        }

        /***************
         * POSITIONING *
         ***************/
        private Rect trueBounds;

        public void updateTrueBounds(Rect newBounds)
        {
            trueBounds = newBounds;
        }

        public Rect getTrueBounds()
        {
            return trueBounds;
        }

        private int getAdjustedXOrientation()
        {
            if (Orientation == 0 || Orientation == 180) return 0;
            if (Orientation > 0 && Orientation < 45) return Orientation;
            if (Orientation >= 45 && Orientation <= 135) return 45;
            if (Orientation < 180 && Orientation > 135) return 45 - (Orientation - 135);
            if (Orientation > 180 && Orientation < 225) return 180 - Orientation;
            if (Orientation > 225 && Orientation < 305) return -45;
            else return Orientation - 360;
        }

        private int getAdjustedYOrientation()
        {
            if (Orientation == 90 || Orientation == 270) return 0;
            if (Orientation <= 45) return -45;
            if (Orientation > 45 && Orientation < 90) return (90 - Orientation) * -1;
            if (Orientation > 90 && Orientation < 135) return Orientation - 90;
            if (Orientation >= 135 && Orientation <= 225) return 45;
            if (Orientation > 225 && Orientation < 270) return 270 - Orientation;
            if (Orientation > 270 && Orientation < 305) return -45 + (305 - Orientation);
            else return -45;
        }

        public Point getFrontBumperPoint()
        {
            Point bumperPoint = new Point();
            double centerX = (trueBounds.Left + trueBounds.Right) / 2, centerY = (trueBounds.Top + trueBounds.Bottom) / 2;
            double adjustedXOrientation = getAdjustedXOrientation(), adjustedYOrientation = getAdjustedYOrientation();
            bumperPoint.X = centerX + ((((trueBounds.Width) / 2) / 45) * adjustedXOrientation);
            bumperPoint.Y = centerY + ((((trueBounds.Height) / 2) / 45) * adjustedYOrientation);
            return bumperPoint;
        }

        public Point getRearBumperPoint()
        {
            Point bumperPoint = new Point();
            double centerX = (trueBounds.Left + trueBounds.Right) / 2, centerY = (trueBounds.Top + trueBounds.Bottom) / 2;
            double adjustedXOrientation = getAdjustedXOrientation() * -1, adjustedYOrientation = getAdjustedYOrientation() * -1;
            bumperPoint.X = centerX + ((((trueBounds.Width) / 2) / 45) * adjustedXOrientation);
            bumperPoint.Y = centerY + ((((trueBounds.Height) / 2) / 45) * adjustedYOrientation);
            return bumperPoint;
        }

        public Point getTrueCenter()
        {
            return new Point((trueBounds.Left + trueBounds.Right) / 2, (trueBounds.Top + trueBounds.Bottom) / 2);
        }

        private static Point newPosition(Point oldPoint, int orientation, double speed)
        {
            return new Point(oldPoint.X + (speed * Math.Sin(degreeToRadian(orientation))),
                oldPoint.Y - (speed * Math.Cos(degreeToRadian(orientation))));
        }

        private static double degreeToRadian(double angle)
        {
            return Math.PI * angle / 180.0;
        }

        /*********************
         * THREAD MANAGEMENT *
         *********************/
        private Thread driveThread = null;

        public void startDriving()
        {
            driveThread = new Thread(drive);
            driveThread.Start();
        }

        public void stopDriving()
        {
            if (driveThread == null) return;
            driveThread.Abort();
            driveThread = null;
        }

        /************
         * MOVEMENT *
         ************/
        private TrafficLight nextTrafficLight;
        private TrafficMarshal trafficMarshal;
        private Direction nextDirection, switchLaneDirection;
        private Lane currentLane, wantedLane;
        private Point nextDestination;
        private Boolean hasDestination, watchTrafficMarshal, onIntersection, mustSwitchLane, turnSignalIsOn;
        private double totalDistance, xDistance, yDistance;

        private int oldOrientation, newOrientation;
        private double maxSpeed, effectiveSpeed;

        private System.Timers.Timer timerTurnSignal;

        private void initDrive()
        {
            nextDirection = Toolkit.RandomEnum<Direction>();
            nextDestination = new Point();
            hasDestination = false;
            watchTrafficMarshal = false;
            turnSignalIsOn = false;
            onIntersection = false;
            mustSwitchLane = false;
            maxSpeed = -1;
            timerTurnSignal = new System.Timers.Timer(200);
            timerTurnSignal.Elapsed += new ElapsedEventHandler(turnSignals);
        }

        private void drive()
        {
            try
            {
                initDrive();
                while (true)
                {
                    DateTime startTime = DateTime.Now; //get starttime
                    Map.INSTANCE.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(doDrive)); //drive
                    updateBounds(); //update bounds
                    DateTime stopTime = DateTime.Now; //get endtime
                    Thread.Sleep(TimeSpan.FromMilliseconds(sleepTime) - (startTime - stopTime)); //sleep
                }
            }
            catch (ThreadAbortException e)
            {
                Trace.WriteLine("Car was stopped :(");
            }
        }

        private void doDrive()
        {
            updateDestination();
            effectiveSpeed = (maxSpeed != -1 && maxSpeed < Speed) ? maxSpeed : Speed;
            checkTrafficLight(); //as of here, check if effectiveSpeed == 0
            checkTrafficMarshal();
            checkCars();
            checkCarsOnOtherLane();
            changePosition();
        }

        private void resetCarState(Boolean cameFromIntersection)
        {
            hasDestination = false;
            nextTrafficLight = null;
            if (cameFromIntersection)
                leftInterSection();
            onIntersection = false;
            currentLane = null;
            wantedLane = null;
            mustSwitchLane = false;
            switchLaneDirection = Direction.FORWARD;
        }

        private void leftInterSection()
        {
            if (watchTrafficMarshal)
            {
                trafficMarshal.removeCar(this);
                watchTrafficMarshal = false;
                trafficMarshal = null;
            }
            nextDirection = Toolkit.RandomEnum<Direction>();
            stopSignals();
        }

        private void updateDestination()
        {
            if (hasDestination)
            {
                Rect bounds = getTrueBounds();
                if (laneToLane())
                    resetCarState(false);
                else if (interSectionToLane())
                    resetCarState(true);
                else if (laneToInterSection())
                    resetCarState(false);
                else return;
            }
            if (!getDestinationFromIntersection())
                getDestinationFromLane();
            oldOrientation = Orientation;
        }

        private Boolean laneToLane()
        {
            Lane newLane = Map.INSTANCE.getLaneAtPoint(getFrontBumperPoint());
            return (currentLane != null && newLane != null && currentLane != newLane && Orientation == newLane.Orientation);
        }

        private Boolean laneToInterSection()
        {
            return (Map.INSTANCE.getInterSectionAtPoint(getFrontBumperPoint()) != null && !onIntersection);
        }

        private Boolean interSectionToLane()
        {
            if (onIntersection &&
                Map.INSTANCE.getInterSectionAtPoint(getFrontBumperPoint()) == null &&
                Map.INSTANCE.getInterSectionAtPoint(getTrueCenter()) == null)
            {
                Lane newLane = Map.INSTANCE.getLaneAtPoint(getRearBumperPoint());
                if (newLane != null && newLane.Orientation == Orientation)
                    return true;
                else if (newLane != null && Toolkit.getOrientationDifference(Orientation, newLane.Orientation) < 4)
                {
                    snapToLane(newLane);
                    return true;
                }
            }
            return false;
        }

        private Boolean getDestinationFromIntersection()
        {
            InterSection interSection = Map.INSTANCE.getInterSectionAtPoint(getFrontBumperPoint());
            if (interSection != null)
            {
                hasDestination = true;
                Lane destinationLane = interSection.getExitLane(new DirectionData(newOrientation, nextDirection), getTrueCenter());
                updateDestination(destinationLane.getStartPoint());
                maxSpeed = destinationLane.MaxSpeed;
                newOrientation = destinationLane.Orientation;
                onIntersection = true;
                setTrafficMarshal(interSection.getTrafficMarshal());
                return true;
            }
            return false;
        }

        private Side findNewSide()
        {
            if ((Orientation == 90 && nextDirection == Direction.LEFT) ||
                (Orientation == 270 && nextDirection == Direction.RIGHT) ||
                (Orientation == 0 && nextDirection == Direction.FORWARD))
                return Side.TOP;
            else if ((Orientation == 90 && nextDirection == Direction.RIGHT) ||
                (Orientation == 270 && nextDirection == Direction.LEFT) ||
                (Orientation == 180 && nextDirection == Direction.FORWARD))
                return Side.BOTTOM;
            else if ((Orientation == 0 && nextDirection == Direction.RIGHT) ||
                (Orientation == 180 && nextDirection == Direction.LEFT) ||
                (Orientation == 90 && nextDirection == Direction.FORWARD))
                return Side.RIGHT;
            else return Side.LEFT;
        }

        private void getDestinationFromLane()
        {
            Lane lane = Map.INSTANCE.getLaneAtPoint(getFrontBumperPoint());
            if (lane != null && correctLane(lane))
            {
                hasDestination = true;
                updateDestination(lane.getDestinationPoint());
                maxSpeed = lane.MaxSpeed;
                newOrientation = lane.Orientation;
                currentLane = lane;
                if (lane.hasTrafficLight())
                {
                    nextTrafficLight = lane.trafficLight;
                    startSignals();
                }
            }
        }

        private Boolean correctLane(Lane lane)
        {
            if (lane.getAllowedDirections().Contains(nextDirection)) //check if current lane allows next direction
                return true;
            Road currentRoad = Map.INSTANCE.getRoadAtPoint(Toolkit.getPointRelativeToMap(lane));
            List<Direction> allowedDirections = getAllowedDirectionsFromRoad(currentRoad);
            while (!allowedDirections.Contains(nextDirection))
                nextDirection = Toolkit.RandomEnum<Direction>();
            switchLane(getCorrectLane(currentRoad));
            currentLane = lane;
            return false;
        }

        private List<Direction> getAllowedDirectionsFromRoad(Road road)
        {
            List<Direction> allowedDirections = new List<Direction>();
            foreach (Lane lane in road.getLanes())
                foreach (Direction direction in lane.getAllowedDirections())
                    if (!allowedDirections.Contains(direction))
                        allowedDirections.Add(direction);
            return allowedDirections;
        }

        private Lane getCorrectLane(Road road)
        {
            foreach (Lane lane in road.getLanes())
                if (lane.getAllowedDirections().Contains(nextDirection))
                    return lane;
            throw new InvalidOperationException("No correct lane exists");
        }

        private void switchLane(Lane lane)
        {
            hasDestination = true;
            newOrientation = lane.Orientation;
            updateDestination(getSwitchLaneDestination(lane));
            maxSpeed = lane.MaxSpeed;
            if (lane.hasTrafficLight())
                nextTrafficLight = lane.trafficLight;
            mustSwitchLane = true;
            wantedLane = lane;
            startSignals();
        }

        private Point getSwitchLaneDestination(Lane lane)
        {
            Point lanePoint = Toolkit.getCenterPoint(lane);
            Point carPoint = getTrueCenter();
            switch (newOrientation)
            {
                case 0:
                    lanePoint.Y = getTrueCenter().Y + (Toolkit.getBounds(this).Height * 1.5); break;
                case 90:
                    lanePoint.X = getTrueCenter().X + (Toolkit.getBounds(this).Width * 1.5); break;
                case 180:
                    lanePoint.Y = getTrueCenter().Y - (Toolkit.getBounds(this).Height * 1.5); break;
                case 270:
                    lanePoint.X = getTrueCenter().X - (Toolkit.getBounds(this).Width * 1.5); break;
                default: throw new InvalidOperationException("newOrientation was invalid");
            }
            switchLaneDirection = getSwitchLaneDirection(carPoint, lanePoint);
            return lanePoint;
        }

        private string getCurrentCarType()
        { 
            string carType = "cara";
            if (Source.ToString().Contains("cara")) { carType = "cara"; }
            else if (Source.ToString().Contains("carb")) { carType = "carb"; }
            else if (Source.ToString().Contains("carc")) { carType = "carc"; }
            else if (Source.ToString().Contains("card")) { carType = "card"; }
            else if (Source.ToString().Contains("care")) { carType = "care"; }
            else if (Source.ToString().Contains("carf")) { carType = "carf"; }
            else if (Source.ToString().Contains("carg")) { carType = "carg"; }
            else if (Source.ToString().Contains("carh")) { carType = "carh"; }
            else if (Source.ToString().Contains("cari")) { carType = "cari"; }
            else if (Source.ToString().Contains("carj")) { carType = "carj"; }
            else if (Source.ToString().Contains("cark")) { carType = "cark"; }
            return carType;
        }

        private Direction getSwitchLaneDirection(Point carPoint, Point lanePoint)
        {
            Direction direction = Direction.FORWARD;
            switch (newOrientation)
            {
                case 0:
                    direction = (carPoint.X > lanePoint.X) ? Direction.LEFT : Direction.RIGHT; break;
                case 90:
                    direction = (carPoint.Y > lanePoint.Y) ? Direction.LEFT : Direction.RIGHT; break;
                case 180:
                    direction = (carPoint.X > lanePoint.X) ? Direction.RIGHT : Direction.LEFT; break;
                case 270:
                    direction = (carPoint.Y > lanePoint.Y) ? Direction.RIGHT : Direction.LEFT; break;
            }
            return direction;
        }

        private void startSignals()
        {
            timerTurnSignal.Enabled = true;
        }

        private void stopSignals()
        {
            timerTurnSignal.Enabled = false;
            turnSignalsOff();
        }

        private void turnSignalsOn()
        {
            if (nextDirection == Direction.LEFT)
                Source = new BitmapImage(new Uri(@"/Master;component/Images/" + getCurrentCarType() + "left.png", UriKind.Relative));
            else if (nextDirection == Direction.RIGHT)
                Source = new BitmapImage(new Uri(@"/Master;component/Images/" + getCurrentCarType() + "right.png", UriKind.Relative));
            turnSignalIsOn = true;
        }

        private void turnSignalsOff()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/" + getCurrentCarType() + ".png", UriKind.Relative));
            turnSignalIsOn = false;
        }

        private void turnSignals(object source, ElapsedEventArgs e)
        {
            this.Dispatcher.Invoke(DispatcherPriority.Render, new Action(delegate()
            {
                if (turnSignalIsOn) turnSignalsOff();
                else turnSignalsOn();
            }));
        }

        private void updateDestination(Point newDestination)
        {
            nextDestination = newDestination;
            Point centerPoint = getTrueCenter();
            xDistance = getXDistance(centerPoint, newDestination);
            yDistance = getYDistance(centerPoint, newDestination);
            totalDistance = xDistance + yDistance;
        }

        private void checkTrafficLight()
        {
            if (nextTrafficLight == null) return;
            Point trafficLightLocation = Toolkit.getPointRelativeToMap(nextTrafficLight);
            Point carCenterLocation = getTrueCenter();
            double distance = 0; double carSize = 0;
            if (newOrientation == 0 || newOrientation == 180)
            {
                distance = trafficLightLocation.Y > carCenterLocation.Y ? trafficLightLocation.Y - carCenterLocation.Y : carCenterLocation.Y - trafficLightLocation.Y;
                carSize = getTrueBounds().Height;
            }
            else
            {
                distance = trafficLightLocation.X > carCenterLocation.X ? trafficLightLocation.X - carCenterLocation.X : carCenterLocation.X - trafficLightLocation.X;
                carSize = getTrueBounds().Width;
            }
            if (distance <= (carSize + maxSpeed) && distance >= carSize)
            {
                atTrafficLight(trafficLightLocation);
            }
        }

        private void atTrafficLight(Point trafficLightLocation)
        {
            if (nextTrafficLight.Status == TrafficLightStatus.RED || nextTrafficLight.Status == TrafficLightStatus.ORANGE)
            {
                effectiveSpeed = 0;
                if (watchTrafficMarshal)
                {
                    trafficMarshal.removeCar(this);
                    watchTrafficMarshal = false;
                }
                return;
            }
            setTrafficMarshal(Map.INSTANCE.getLaneAtPoint(trafficLightLocation).getInterSection().getTrafficMarshal());
        }

        private void setTrafficMarshal(TrafficMarshal trafficMarshal)
        {
            if (!watchTrafficMarshal && trafficMarshal != null)
            {
                this.trafficMarshal = trafficMarshal;
                trafficMarshal.registerCar(new DirectionData(Orientation, nextDirection), this);
                watchTrafficMarshal = true;
            }
        }

        private void checkTrafficMarshal()
        {
            if (!watchTrafficMarshal || effectiveSpeed <= 0) 
                return;
            if (!trafficMarshal.canMoveForward(this))
                effectiveSpeed = 0;
        }

        private void checkCars()
        {
            if (effectiveSpeed == 0) return; //already stopped, no need to check for cars
            double distance = 0;
            if (Orientation == 270 || Orientation == 90)
                distance = getTrueBounds().Width;
            else if (Orientation == 0 || Orientation == 180)
                distance = getTrueBounds().Height;
            else return;
            if (Map.INSTANCE.getCarAtPoint(newPosition(getTrueCenter(), Orientation, distance)) != null)
                effectiveSpeed = 0;
        }

        private void checkCarsOnOtherLane()
        {
            if (effectiveSpeed == 0 || !mustSwitchLane || wantedLane == null) return; //already stopped or not switching lanes, no need to check for cars
            Rect bounds = new Rect();
            switch (newOrientation)
            {
                case 0:
                    bounds = new Rect(new Point(Toolkit.getCenterPoint(wantedLane).X, getFrontBumperPoint().Y - (getTrueBounds().Height / 2)),
                        new Point(Toolkit.getCenterPoint(wantedLane).X, getRearBumperPoint().Y + (getTrueBounds().Height / 2))); break;
                case 90:
                    bounds = new Rect(new Point(getFrontBumperPoint().X + (getTrueBounds().Width / 2), Toolkit.getCenterPoint(wantedLane).Y),
                        new Point(getFrontBumperPoint().X - (getTrueBounds().Width / 2), Toolkit.getCenterPoint(wantedLane).Y)); break;
                case 180:
                    bounds = new Rect(new Point(Toolkit.getCenterPoint(wantedLane).X, getFrontBumperPoint().Y + (getTrueBounds().Height / 2)),
                        new Point(Toolkit.getCenterPoint(wantedLane).X, getRearBumperPoint().Y - (getTrueBounds().Height / 2))); break;
                case 270:
                    bounds = new Rect(new Point(getFrontBumperPoint().X - (getTrueBounds().Width / 2), Toolkit.getCenterPoint(wantedLane).Y),
                        new Point(getFrontBumperPoint().X + (getTrueBounds().Width / 2), Toolkit.getCenterPoint(wantedLane).Y)); break;
                default: throw new InvalidOperationException("newOrientation was invalid");
            }
            checkCarsOnOtherLane(bounds);
        }

        private void checkCarsOnOtherLane(Rect bounds)
        {
            List<Car> cars = Map.INSTANCE.getCarsAtBounds(bounds);
            if (cars.Contains(this)) cars.Remove(this);
            if (cars.Count > 0)
                effectiveSpeed = 0;
        }

        private void snapToLane(Lane lane)
        {
            Point currentCenterPoint = getTrueCenter();
            Point wantedCenterPoint = new Point(currentCenterPoint.X, currentCenterPoint.Y);
            if (lane.Orientation == 90 || lane.Orientation == 270)
            {
                Point laneLocation = Toolkit.getPointRelativeToMap(lane);
                wantedCenterPoint.Y = ((laneLocation.Y * 2) + lane.ActualHeight) / 2;
            }
            else
            {
                Point laneLocation = Toolkit.getPointRelativeToMap(lane);
                wantedCenterPoint.X = ((laneLocation.X * 2) + lane.ActualWidth) / 2;
            }
            Orientation = lane.Orientation;
            Position = new Point(Position.X + (wantedCenterPoint.X - currentCenterPoint.X), Position.Y + (wantedCenterPoint.Y - currentCenterPoint.Y));
        }

        private void changePosition()
        {
            if (effectiveSpeed <= 0) return;
            if (mustSwitchLane)
                doSwitchLane();
            else if ((!hasDestination || newOrientation == oldOrientation)) //no destination set or new destination is straight ahead
                Position = newPosition(Position, Orientation, effectiveSpeed);
            else
                moveTowardDestination();
        }

        private void doSwitchLane()
        {
            double distanceFactor = 0;
            if (newOrientation == 180 || newOrientation == 0)
                distanceFactor = getXDistance(getTrueCenter(), nextDestination) / xDistance;
            else
                distanceFactor = getYDistance(getTrueCenter(), nextDestination) / yDistance;
            Orientation = generateSwitchLaneOrientation(distanceFactor);
            Position = newPosition(Position, Orientation, effectiveSpeed);
        }

        private QuinticEase slEase = new QuinticEase(); //switch lane ease

        private int generateSwitchLaneOrientation(double distanceFactor)
        {
            double currentDifference = Toolkit.getOrientationDifference(newOrientation, Orientation);
            int directionFactor = (switchLaneDirection == Direction.LEFT) ? -1 : 1;
            if (distanceFactor >= 0.5) 
                currentDifference = (currentDifference + (Speed / 2) > 45) ? 45 : currentDifference + (Speed / 2);
            else
            {
                distanceFactor *= 2;
                slEase.EasingMode = EasingMode.EaseOut;
                currentDifference = slEase.Ease(distanceFactor) * 45;
            }
            return Toolkit.alterOrientationBy(newOrientation, (int) (currentDifference * directionFactor));
        }
        
        private double getXDistance(Point a, Point b)
        {
            double xDistance = a.X - b.X;
            if (xDistance < 0) xDistance *= -1;
            return xDistance;
        }

        private double getYDistance(Point a, Point b)
        {
            double yDistance = a.Y - b.Y;
            if (yDistance < 0) yDistance *= -1;
            return yDistance;
        }

        private double getTotalDistance(Point a, Point b)
        {
            return getXDistance(a, b) + getYDistance(a, b);
        }

        private EasingFunctionBase m2Dease = new QuadraticEase(); //move 2 destination ease

        private void moveTowardDestination()
        {
            //m2Dease.Power = 2;
            double xSpeed = 0, ySpeed = 0, currentDistance = 0, distanceEaseResult = 0;
            if (oldOrientation == 180 || oldOrientation == 0)
            {
                currentDistance = getYDistance(getTrueCenter(), nextDestination);
                distanceEaseResult = m2Dease.Ease(currentDistance / yDistance);
            }
            else
            {
                currentDistance = getXDistance(getTrueCenter(), nextDestination);
                distanceEaseResult = m2Dease.Ease(currentDistance / xDistance);
            }
            xSpeed = getXSpeed(distanceEaseResult);
            ySpeed = getYSpeed(distanceEaseResult);
            generateNewOrientation(distanceEaseResult);
            Position = new Point(Position.X + xSpeed, Position.Y + ySpeed);
        }

        private double getXSpeed(double easeResult)
        {
            if (oldOrientation == 90)
                return effectiveSpeed * easeResult;
            if (oldOrientation == 270)
                return (effectiveSpeed * easeResult) * -1;
            if (newOrientation == 90)
                return effectiveSpeed * (1 - easeResult);
            else
                return (effectiveSpeed * (1 - easeResult)) * -1;
        }

        private double getYSpeed(double easeResult)
        {
            if (oldOrientation == 0) //car was moving towards the top of the screen
                return (effectiveSpeed * easeResult) * -1;
            if (oldOrientation == 180) //car was moving towards the bottom of the screen
                return effectiveSpeed * easeResult;
            if (newOrientation == 0) //car is turning towards the top of the screen
                return (effectiveSpeed * (1 - easeResult)) * -1;
            else //car is turning towards the bottom of the screen
                return (effectiveSpeed * (1 - easeResult));
        }

        private void generateNewOrientation(double easeResult)
        {
            if (nextDirection == Direction.RIGHT)
                Orientation = Toolkit.alterOrientationBy(oldOrientation, (int)(90 * (1 - easeResult)));
            else
                Orientation = Toolkit.alterOrientationBy(oldOrientation, (int)(-90 * (1 - easeResult)));
        }

        //force re-rendering (--> this ACTUALLY updates the position and re-draws the car with the new data) and draw hitbox
        private void updateBounds()
        {
            this.Dispatcher.Invoke(DispatcherPriority.Render,
                new Action(delegate()
                {
                    updateTrueBounds(getNewBounds(this));
                }
            ));
        }

        //drawhitbox & everything related to it are prototypes to try and get the true coordinates and dimensions of the transformed car
        private Line[] lines, lines2;

        private static Rect getNewBounds(Car thisCar)
        {
            Rect bounds = Toolkit.getBounds(thisCar);

            /*/-- THIS SECTION IS OPTIONAL AND WILL BE REMOVED AT A LATER STAGE --
             drawHitBox(thisCar, bounds);
             drawShitBox(thisCar);
             //--------------------- END OF OPTIONAL SECTION ---------------------*/

            return bounds;
        }

        //HITBOX = actual size (after applying transformations)
        private static void drawHitBox(Car thisCar, Rect bounds)
        {
            if (thisCar.lines != null) foreach (Line line in thisCar.lines) Map.INSTANCE.Children.Remove(line);
            thisCar.lines = new Line[4];
            thisCar.lines[0] = createLine(bounds.TopLeft, bounds.TopRight, false);
            thisCar.lines[1] = createLine(bounds.TopRight, bounds.BottomRight, false);
            thisCar.lines[2] = createLine(bounds.BottomRight, bounds.BottomLeft, false);
            thisCar.lines[3] = createLine(bounds.BottomLeft, bounds.TopLeft, false);
        }

        //SHITBOX = 'WPF' size (dimensions according to WPF, these do not consider transformations)
        private static void drawShitBox(Car thisCar)
        {
            Point currentPoint = thisCar.Position;
            Rect bounds = new Rect(currentPoint.X, currentPoint.Y, thisCar.ActualWidth, thisCar.ActualHeight);

            if (thisCar.lines2 != null) foreach (Line line in thisCar.lines2) Map.INSTANCE.Children.Remove(line);
            thisCar.lines2 = new Line[4];
            thisCar.lines2[0] = createLine(bounds.TopLeft, bounds.TopRight, true);
            thisCar.lines2[1] = createLine(bounds.TopRight, bounds.BottomRight, true);
            thisCar.lines2[2] = createLine(bounds.BottomRight, bounds.BottomLeft, true);
            thisCar.lines2[3] = createLine(bounds.BottomLeft, bounds.TopLeft, true);
        }

        private static Line createLine(Point startPoint, Point endPoint, Boolean isShitBox)
        {
            Line myLine = new Line();
            myLine.Stroke = (isShitBox) ? System.Windows.Media.Brushes.SaddleBrown : System.Windows.Media.Brushes.LightSteelBlue;
            myLine.X1 = startPoint.X; myLine.X2 = endPoint.X;
            myLine.Y1 = startPoint.Y; myLine.Y2 = endPoint.Y;
            myLine.StrokeThickness = 2;
            Map.INSTANCE.Children.Add(myLine);
            return myLine;
        }
    }

    public class RedCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/cara.png", UriKind.Relative));
        }
    }

    public class BlueCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/carb.png", UriKind.Relative));
        }
    }

    public class PinkCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/carc.png", UriKind.Relative));
        }
    }

    public class BrownCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/card.png", UriKind.Relative));
        }
    }

    public class PoliceCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/care.png", UriKind.Relative));
        }
    }

    public class GreenCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/carf.png", UriKind.Relative));
        }
    }

    public class YellowCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/carg.png", UriKind.Relative));
        }
    }

    public class YellowSportsCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/carh.png", UriKind.Relative));
        }
    }

    public class RedSportsCar : Car
    {
        protected override void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/cari.png", UriKind.Relative));
        }
    }
    public class WhiteCar : Car
    {
        protected override void initCarImage()
        {
            Scale = 0.30;
            Source = new BitmapImage(new Uri(@"/Master;component/Images/carj.png", UriKind.Relative));
        }
    }

    /**
     * CarFactory can be used to create cars of any/all available car-types.
     * 
     * Methodname's of methods that create new cars should always start with "create" and end with "Car".
     * Car-types created by methods that do not adhere to the above rule will not be included in the "createRandomCar"-method.
     * */
    public class CarFactory
    {
        private static List<MethodInfo> methods;
        private static Random random = new Random();

        static CarFactory()
        {
            methods = new List<MethodInfo>();
            foreach (MethodInfo method in typeof(CarFactory).GetMethods())
            {
                if (method.Name == "createRandomCar")
                    continue;
                if (method.Name.StartsWith("create") && method.Name.EndsWith("Car"))
                    methods.Add(method);
            }
        }

        public static Car createRandomCar()
        {
            return (Car) methods[random.Next(0, methods.Count)].Invoke(null, null);
        }

        public static Car createRedCar() { return new RedCar(); }
        public static Car createBlueCar() { return new BlueCar(); }
        public static Car createPinkCar() { return new PinkCar(); }
        public static Car createBrownCar() { return new BrownCar(); }
        public static Car createPoliceCar() { return new PoliceCar(); }
        public static Car createGreenCar() { return new GreenCar(); }
        public static Car createYellowCar() { return new YellowCar(); }
        public static Car createYellowSportsCar() { return new YellowSportsCar(); }
        public static Car createRedSportsCar() { return new RedSportsCar(); }
        public static Car createWhiteCar() { return new WhiteCar(); }
    }
}